﻿using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.Data.SqlClient;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Runtime.Serialization;
using System.Text;
using System.Threading;
using Microsoft.Build.Evaluation;
using Microsoft.Build.Execution;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using Microsoft.TeamFoundation.Build.Client;
using Microsoft.VisualStudio.ArchitectureTools.CodeRepositoryQuery;
using PowersourceUIToolkit;
using UCL.Powersource.ServiceReference.RemoteBuildBridge;


namespace UCL.Powersource.TFS
{

    public class MSBuildLogger : Logger
    {
        ILog _Log;
        public ILog Log { get { return _Log ?? (_Log = LogManager.GetLog(this.GetType())); } }

        Guid IdLogger = Guid.Empty;

        public MSBuildLogger()
        {
            IdLogger = Guid.NewGuid();
        }

        public override void Initialize(Microsoft.Build.Framework.IEventSource eventSource)
        {
            eventSource.AnyEventRaised += eventSource_AnyEventRaised;
        }

        void eventSource_AnyEventRaised(object sender, Microsoft.Build.Framework.BuildEventArgs e)
        {
            Log.Info("MSBUILD [{2}]: {0} - {1}", e.Timestamp, e.Message, IdLogger);
        }

        public override void Shutdown()
        {
            Log.Info("MSBUILD SHUTDOWN [{1}]: {0} ", DateTime.Now, IdLogger);
            base.Shutdown();
        }
    }

    [DataContract]
    internal class AnalysisBridge : InitializedLifeCycleBase, IAnalysisBridge, IEvaluateForceRecalculate
    {

        private IPowersourceExt IPowersourceExt;
        public EvaluateForceRecalculateDelegate EvaluateForceRecalculate { get; set; }

        private bool EvaluateForceRecalculateInvokeOrDefault(bool forceRecalculate, ITfsTeamProjectConfiguration teamProject, ITimeFrame tf)
        {
            if (EvaluateForceRecalculate == null) return forceRecalculate;
            return EvaluateForceRecalculate(forceRecalculate, teamProject, tf);
        }

        internal static class Conventions
        {
            internal static string _codeIndexProject;
            internal static string codeIndexProject
            {
                get
                {
                    if (_codeIndexProject == null)
                    {
                        string programFiles;

                        if (Environment.GetEnvironmentVariables().Contains("ProgramFiles(x86)"))
                            programFiles = Environment.GetEnvironmentVariable("ProgramFiles(x86)");
                        else
                            programFiles = Environment.GetEnvironmentVariable("ProgramFiles");

                        _codeIndexProject = programFiles + "\\MSBuild\\Microsoft\\VisualStudio\\v11.0\\Repository\\Microsoft.Repository.targets";
                    }

                    return _codeIndexProject;
                }
            }
            internal static string repositoryName = "PS.{0}.{1}.{2}";

            internal static string checkDBFormatString = "IF  EXISTS (SELECT name FROM sys.databases WHERE name = N'{0}') ";
            internal static string dropBackupBase = "{1} EXEC msdb.dbo.sp_delete_database_backuphistory @database_name = N'{0}'";
            internal static string singleUserBase = "{1} ALTER DATABASE [{0}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE";
            internal static string dropDBBase = "{1} DROP DATABASE [{0}]";

            internal static string GetLocalPathFor(ITfsTeamProjectCollectionConfiguration tfsCollConfig, ITfsTeamProjectConfiguration project, ITimeFrame timeframe)
            {
                return tfsCollConfig.LocalDropBasePath + "\\" + tfsCollConfig.Name + "\\" + project.UniqueName + "\\" + timeframe.Label.Replace("\\", "_"); ;
            }

            internal static IEnumerable<string> GetDropDBCommandsFor(string repository)
            {
                List<string> commandList = new List<string>();
                var checkDBstring = string.Format(checkDBFormatString, repository);
                commandList.Add(string.Format(dropBackupBase, repository, checkDBstring));
                commandList.Add(string.Format(singleUserBase, repository, checkDBstring));
                commandList.Add(string.Format(dropDBBase, repository, checkDBstring));

                return commandList;
            }

            internal static string GetRepositoryNameFor(ITfsTeamProjectCollectionConfiguration tfsCollConfig, ITfsTeamProjectConfiguration project, ITimeFrame timeframe)
            {
                return string.Format(repositoryName, tfsCollConfig.Name, project.UniqueName, timeframe.Label.Replace("\\", "."));
            }
        }

        public AnalysisBridge(TfsTeamProjectConfiguration teamProjConfig)
        {
            _TeamProject = teamProjConfig;

        }

        //private ILog Log;

        [DataMember]
        private TfsTeamProjectConfiguration _TeamProject;

        public ITfsTeamProjectConfiguration TeamProject { get { return _TeamProject; } }

        private ITfsTeamProjectCollectionConfiguration TeamProjectCollectionConfiguration { get { return _TeamProject.TeamProjectCollectionConfiguration; } }

        [DataMember]
        private Dictionary<ITimeFrame, string> CodeIndexConnectionStringList;

        private Dictionary<ITimeFrame, RepositoryEntities> CodeIndexList;
        private IRemoteBuilderQueue _RemoteBuilderQueue;
        private IRemoteBuilderQueue IRemoteBuilderQueue
        {
            get { return _RemoteBuilderQueue ?? (_RemoteBuilderQueue = IoC.Get<IRemoteBuilderQueue>()); }
        }

        public RepositoryEntities RetrieveCodeIndexOrEnqueue(ITimeFrame timeFrame, bool forceRecalculate = false)
        {

            if (CodeIndexList == null ||
                !CodeIndexList.ContainsKey(timeFrame) ||
                CodeIndexList[timeFrame] == null ||
                forceRecalculate
                )
            {
                if (!IsInitialized) Initialize();
                if (IPowersourceExt.StatisticManager != null && IPowersourceExt.StatisticManager.StatisticManagerQueue != null)
                {
                    if (
                        IPowersourceExt.StatisticManager.StatisticManagerQueue.IsCurrentThreadUIThread(Thread.CurrentThread)
                        &&
                        IRemoteBuilderQueue != null
                        )
                    {
                        IRemoteBuilderQueue.GetBuildAsync(
                            this.TeamProject, timeFrame, forceRecalculate,
                            OnCompletedCallback: () =>
                                {
                                    var codeIndex = RetrieveCodeIndex(timeFrame, forceRecalculate);
                                    if (codeIndex == null)
                                    {
                                        Log.Warn("CodeIndex for {0} {1} unavailable", this.TeamProject.Label, timeFrame.Label);
                                    }
                                }
                            );
                        return null;
                    }
                    else //we are already on non UI thread
                    {
                        return RetrieveCodeIndex(timeFrame, forceRecalculate);
                    }
                }
                else //we cannot do anything just fire it on current thread
                {
                    return RetrieveCodeIndex(timeFrame, forceRecalculate);
                }
            }
            else
            {
                if (CodeIndexList[timeFrame].Connection != null && (CodeIndexList[timeFrame].Connection.State == System.Data.ConnectionState.Broken))
                {
                    CodeIndexList[timeFrame] = null;
                    string conStr = null;
                    try
                    {
                        conStr = RetrieveCodeIndexConnectionString(timeFrame, forceRecalculate);

                    }
                    catch (Exception exc)
                    {
                        Log.Error(exc, "Instantiating the connection to SQL thrown an exception timeFrame:{0} - forceRecalculate: {1}", timeFrame, forceRecalculate);
                        //throw;
                    }
                    EnsureCodeIndexConnectionValid(timeFrame, conStr);

                }

                return CodeIndexList[timeFrame];
            }
        }

        public RepositoryEntities RetrieveCodeIndex(ITimeFrame timeFrame, bool forceRecalculate = false)
        {

            if (!IsInitialized) Initialize();

            if (CodeIndexList == null) CodeIndexList = new Dictionary<ITimeFrame, RepositoryEntities>();

            if (!CodeIndexList.ContainsKey(timeFrame) ||
                CodeIndexList[timeFrame] == null ||
                CodeIndexList[timeFrame].Connection == null ||
                CodeIndexList[timeFrame].Connection.State == System.Data.ConnectionState.Broken ||
                EvaluateForceRecalculateInvokeOrDefault(forceRecalculate, this.TeamProject, timeFrame)
                )
            {
                string conStr = null;
                try
                {
                    conStr = RetrieveCodeIndexConnectionString(timeFrame, forceRecalculate);

                }
                catch (Exception exc)
                {
                    Log.Error(exc, "Instantiating the connection to SQL thrown an exception timeFrame:{0} - forceRecalculate: {1}", timeFrame, forceRecalculate);
                    //throw;
                }
                EnsureCodeIndexConnectionValid(timeFrame, conStr);
            }
            else
            {
                //Nothing to work on - CodeIndex is there and valid
                //if (CodeIndexList[timeFrame].Connection==null || CodeIndexList.c)
                //EnsureCodeIndexConnectionValid(timeFrame, null);
            }
            return CodeIndexList[timeFrame];

        }

        private void EnsureCodeIndexConnectionValid(ITimeFrame timeFrame, string conStr)
        {
            if (!IsInitialized) Initialize();

            if (!CodeIndexList.ContainsKey(timeFrame))
            {
                if (conStr != null)
                {
                    CodeIndexList.Add(timeFrame, Utilities.CreateNewDBContext(conStr));
                }
            }
            else
            {
                if (CodeIndexList[timeFrame] != null)
                {
                    if (CodeIndexList[timeFrame].Connection == null)
                    {
                        CodeIndexList[timeFrame].Dispose();
                        CodeIndexList[timeFrame] = null;
                        if (conStr != null) CodeIndexList[timeFrame] = Utilities.CreateNewDBContext(conStr);
                    }
                    else
                        if (
                            CodeIndexList[timeFrame].Connection.State == System.Data.ConnectionState.Broken
                            )
                        {
                            CodeIndexList[timeFrame].Dispose();
                            CodeIndexList[timeFrame] = null;
                            if (conStr != null) CodeIndexList[timeFrame] = Utilities.CreateNewDBContext(conStr);
                        }
                        else
                        {
                            //Nothing to do CodeIndexList[timeFrame] exists and it uses the same connection string
                            //   CodeIndexList[timeFrame] = Utilities.CreateNewDBContext(conStr);
                        }
                }
                else
                {
                    CodeIndexList[timeFrame] = Utilities.CreateNewDBContext(conStr);
                }
            }

        }

        //public void ClearConnectionStringAndConnection(TimeFrame tf)
        //{
            
        //    lock (this)
        //    {
        //        if (CodeIndexList == null) return;
        //        if (CodeIndexList != null && CodeIndexList.ContainsKey(tf))
        //        {
        //            if (CodeIndexList[tf] != null)
        //            {
        //                CodeIndexList[tf].Dispose();
        //            }
                    
        //            CodeIndexList[tf] = null;
        //            CodeIndexList.Remove(tf);
        //        }

        //        if (CodeIndexConnectionStringList!=null && CodeIndexConnectionStringList.ContainsKey())
                
        //    }
        //}

        private string RetrieveCodeIndexConnectionString(ITimeFrame timeFrame, bool forceRecalculate = false)
        {
            if (!IsInitialized) Initialize();
            if (CodeIndexConnectionStringList == null) CodeIndexConnectionStringList = new Dictionary<ITimeFrame, string>();

            string innerPath = Conventions.GetLocalPathFor(TeamProjectCollectionConfiguration, TeamProject, timeFrame);
            string repository = Conventions.GetRepositoryNameFor(TeamProjectCollectionConfiguration, TeamProject, timeFrame);

            var forceRecalculateEvaluated = EvaluateForceRecalculateInvokeOrDefault(forceRecalculate, this.TeamProject, timeFrame);

            if (!forceRecalculate && Directory.Exists(innerPath)) // if we are not recalculating we first try to test if repository string exists
            {
                
                string connectionString = CreateConnectionString(TeamProjectCollectionConfiguration.CodeIndexDBServer, repository);
                try
                {
                    using (var ent = Utilities.CreateNewDBContext(connectionString))
                    {
                        var cntAssemblies = ent.Assemblies.Count();
                        if (cntAssemblies > 0) // if we have some assemblies we assume we don't need to recalculate
                        {
                            AddConnectionToCodeIndexConnectionStringList(timeFrame, connectionString);
                        }
                    }
                }
                catch (Exception exc)
                {
                    Log.Warn(exc,"For existing inner path {0} code index {1} did not exists. Code index will be repopulated!",innerPath, connectionString);   
                    //throw;
                }

            }

            if (!Directory.Exists(innerPath) || !CodeIndexConnectionStringList.ContainsKey(timeFrame) || forceRecalculateEvaluated)
            {

                if (!Directory.Exists(innerPath) || forceRecalculateEvaluated )
                    RequestFiles(timeFrame, innerPath);

                //insert string in the dictionary before the call to clear so it will delete appropriate database
                string connectionString = CreateConnectionString(TeamProjectCollectionConfiguration.CodeIndexDBServer, repository);
                AddConnectionToCodeIndexConnectionStringList(timeFrame, connectionString);

                if (CodeIndexConnectionStringList.ContainsKey(timeFrame))
                    ClearOldCodeIndex(CodeIndexConnectionStringList[timeFrame], repository);

                //msbuild code index on the files
                if (!PopulateCodeIndex(innerPath, repository))
                {
                    Log.Error(null, "Populate code index for {0} on {1} failed. Using old one!", innerPath, repository);
                }

            }

            return CodeIndexConnectionStringList[timeFrame];
        }

        private void AddConnectionToCodeIndexConnectionStringList(ITimeFrame timeFrame, string connectionString)
        {
            if (!CodeIndexConnectionStringList.ContainsKey(timeFrame))
                CodeIndexConnectionStringList.Add(timeFrame, connectionString);
            else
                CodeIndexConnectionStringList[timeFrame] = connectionString;
        }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2100:Review SQL queries for security vulnerabilities")]
        private void ClearOldCodeIndex(string connString, string repository)
        {
            if (!IsInitialized) Initialize();
            string masterConn = connString.Replace(repository, "master");
            IEnumerable<string> commandList = Conventions.GetDropDBCommandsFor(repository);

            using (var connection = new SqlConnection(masterConn))
            {
                connection.Open();

                foreach (var command in commandList)
                {
                    using (var deleteCommand = new SqlCommand(command, connection))
                    {
                        deleteCommand.ExecuteNonQuery();
                    }
                }

            }
        }


        private string CreateConnectionString(string serverDB, string repository)
        {
            if (!IsInitialized) Initialize();
            SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder(); ;
            builder.InitialCatalog = repository;
            builder.DataSource = serverDB;
            builder.MultipleActiveResultSets = true;
            builder.IntegratedSecurity = true;
            builder.ConnectTimeout = 120; //Increased timeout

            return builder.ConnectionString;
        }


        private bool PopulateCodeIndex(string innerPath, string repository)
        {
            if (!IsInitialized) Initialize();
            Log.Debug("PopulateCodeIndex on {0} from repository {1}", innerPath, repository);
            ProjectCollection pc = new ProjectCollection();

            Dictionary<string, string> GlobalProperty = new Dictionary<string, string>();

            GlobalProperty.Add("LoadFileSpec", string.Format("{0}\\**\\*.dll;{0}\\**\\*.exe", innerPath));
            GlobalProperty.Add("Repository", repository);
            GlobalProperty.Add("RepositoryServer", TeamProjectCollectionConfiguration.CodeIndexDBServer);

            BuildRequestData b = new BuildRequestData(Conventions.codeIndexProject, GlobalProperty, "4.0", new string[] { "LoadFiles" }, null);
            
            var log = new MSBuildLogger();
            var bpars = new BuildParameters(pc);
            bpars.Loggers = new ILogger[] { log };
            bpars.DisableInProcNode = true;
            BuildManager.DefaultBuildManager.BeginBuild(bpars);
            
            var sub = BuildManager.DefaultBuildManager.PendBuildRequest(b);
            sub.ExecuteAsync(null, null);

            while (!sub.IsCompleted)
            {
                Thread.Sleep(500);
            }

            var res = sub.BuildResult;
            BuildManager.DefaultBuildManager.EndBuild();
            if (res.OverallResult == BuildResultCode.Success) return true;
            return false;

            //var buildFinished = false;
            //sub.ExecuteAsync((a) => { buildFinished = true; }, null);

            //while (!buildFinished && !sub.IsCompleted)
            //{
            //    Thread.Sleep(500);
            //}
            //var res = sub.BuildResult;
            //BuildManager.DefaultBuildManager.EndBuild();
            //if (res.OverallResult == BuildResultCode.Success) return true;
            //return false;

        }

        [DataMember]
        private Dictionary<ITimeFrame, List<string>> DisassemblyFileList;

        public IEnumerable<string> RetrieveDisassemblyFiles(ITimeFrame timeFrame, bool forceRecalculate = false)
        {
            if (!IsInitialized) Initialize();
            if (DisassemblyFileList == null) DisassemblyFileList = new Dictionary<ITimeFrame, List<string>>();

            if (!DisassemblyFileList.ContainsKey(timeFrame) || EvaluateForceRecalculateInvokeOrDefault(forceRecalculate, this.TeamProject, timeFrame))
            {
                string innerPath = Conventions.GetLocalPathFor(TeamProjectCollectionConfiguration, TeamProject, timeFrame);

                if (!Directory.Exists(innerPath) || forceRecalculate)
                    RequestFiles(timeFrame, innerPath);

                if (!RunDisassemblerFor(innerPath)) return null;

                if (!DisassemblyFileList.ContainsKey(timeFrame))
                    DisassemblyFileList.Add(timeFrame, Directory.EnumerateFiles(innerPath, "*.il", SearchOption.AllDirectories).ToList());
                else
                    DisassemblyFileList[timeFrame] = Directory.EnumerateFiles(innerPath, "*.il", SearchOption.AllDirectories).ToList();
            }

            return DisassemblyFileList[timeFrame];
        }

        private bool RunDisassemblerFor(string path)
        {
            if (!IsInitialized) Initialize();
            Log.Debug("Running disassembler for {0}", path);
            List<string> availableFiles = new List<string>();
            availableFiles.AddRange(Directory.EnumerateFiles(path, "*.dll", SearchOption.AllDirectories));
            availableFiles.AddRange(Directory.EnumerateFiles(path, "*.exe", SearchOption.AllDirectories));
            string fileName;
            foreach (string file in availableFiles)
            {
                using (Process ildasm = new Process())
                {
                    ildasm.StartInfo.FileName = TeamProjectCollectionConfiguration.IldasmPath;
                    fileName = GetPureFileName(file);
                    ildasm.StartInfo.Arguments = string.Format("\"{0}\" /output:\"{1}.il\"", file, fileName);
                    ildasm.StartInfo.UseShellExecute = false;
                    ildasm.StartInfo.RedirectStandardOutput = true;
                    ildasm.StartInfo.RedirectStandardError = true;
                    ildasm.OutputDataReceived += ildasm_OutputDataReceived;
                    ildasm.ErrorDataReceived += ildasm_ErrorDataReceived;
                    ildasm.Start();
                    ildasm.BeginErrorReadLine(); ildasm.BeginOutputReadLine();
                    ildasm.WaitForExit();
                }
            }

            return true;
        }

        void ildasm_ErrorDataReceived(object sender, DataReceivedEventArgs e)
        {
            if (!string.IsNullOrWhiteSpace(e.Data)) Log.Error(null, e.Data);
        }

        void ildasm_OutputDataReceived(object sender, DataReceivedEventArgs e)
        {
            if (!string.IsNullOrWhiteSpace(e.Data)) Log.Trace(e.Data);
        }

        private bool RequestFiles(ITimeFrame timeFrame, string innerPath)
        {
             if (!IsInitialized) Initialize();
            Log.Debug("Requesting files for {0} to be downloaded to {1}", timeFrame, innerPath);
            //Fire bd on tfs
            IRemoteBuilderQueueItem build=null;
            try
            {
                build = IRemoteBuilderQueue.GetBuild(TeamProject, timeFrame,false);
                
            }
            catch (Exception exc)
            {
                Log.Error(exc, "Retrieve files - GetBuild failed!");
            }

            if (Directory.Exists(innerPath))
                Directory.Delete(innerPath, true);
            
            

            if (build != null && build.Status == QueuedItemStatus.Finished)
            {
                Directory.CreateDirectory(innerPath);
                var ibb = RemoteBuildBridgeHelper.CreateConnectionToBuildBridgeService(build.Project.TeamProjectCollectionConfiguration.WebServiceUrl);

                var tempZip = innerPath + "\\tempFile";

                //CHECK IF THE NAME IS FINE
                using (var s = ibb.GetRemoteBuild(TeamProject.UniqueGuid, build.BuildDetail.BuildDefinition.Name, build.BuildDetail.BuildNumber))
                {
                    if (!Directory.Exists(innerPath)) Directory.CreateDirectory(innerPath);

                    using (var tempFile = File.Open(tempZip, FileMode.Create, FileAccess.Write))
                    {
                        s.CopyTo(tempFile);
                    }
                }

                ibb.Close();

                ZipFile.ExtractToDirectory(tempZip, innerPath);
                File.Delete(tempZip);
                return true;
            }
            return false;
        }

        private string GetPureFileName(string file)
        {
            string str = null;
            if (file.EndsWith(".dll")) str = file.Split(new string[] { ".dll" }, StringSplitOptions.None).First();
            else if (file.EndsWith(".exe")) str = file.Split(new string[] { ".exe" }, StringSplitOptions.None).First();

            return str;
        }


        protected override bool OnInitializing()
        {
            Log = LogManager.GetLog(typeof(AnalysisBridge));
            IPowersourceExt = IoC.Get<IPowersourceExt>();
            return true;
        }

        protected override void OnDisposing()
        {
            if (CodeIndexList != null)
            {
                foreach (var cil in CodeIndexList)
                {
                    if (cil.Value != null) cil.Value.Dispose();
                }
            }
            Log = null;
        }
    }
}
